using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;

namespace LightCAD.Three
{
    public interface IAnimationObject
    {
        string uuid { get; set; }
    }
    public class AnimationObjectGroup : IAnimationObject
    {
        public class Status
        {
            private AnimationObjectGroup scope;
            public Objects objects;
            public Status(AnimationObjectGroup scope)
            {
                this.scope = scope;
                this.objects = new Objects(scope);
            }
            public int bindingsPerObject
            {
                get
                {
                    return scope._bindings.Length;
                }
            }
        }

        public class Objects
        {
            private AnimationObjectGroup scope;
            public Objects(AnimationObjectGroup scope)
            {
                this.scope = scope;
            }
            public int total
            {
                get
                {
                    return scope._objects.Length;
                }
            }
            public int inUse
            {
                get
                {
                    return this.total - scope.nCachedObjects_;
                }
            }

        }

        #region Properties

        public string uuid { get; set; }
        public ListEx<Object3D> _objects;
        public int nCachedObjects_;
        public JsObj<int?> _indicesByUUID;
        public ListEx<string> _paths;
        public ListEx<PropertyBinding.ParseResult> _parsedPaths;
        public ListEx<ListEx<PropertyBinding>> _bindings;
        public JsObj<int?> _bindingsIndicesByPath;
        public object stats;

        #endregion

        #region constructor
        public AnimationObjectGroup(params Object3D[] arguments)
        {
            this.uuid = MathEx.GenerateUUID();
            // cached objects followed by the active ones
            //this._objects = Array.prototype.slice.call(arguments);
            this._objects = arguments.ToListEx();
            this.nCachedObjects_ = 0; // threshold
                                      // note: read by PropertyBinding.Composite
            var indices = new JsObj<int?> { };
            this._indicesByUUID = indices; // for bookkeeping
            for (int i = 0, n = arguments.Length; i != n; ++i)
            {
                indices[arguments[i].uuid] = i;
            }
            this._paths = new ListEx<string>(); // inside: string
            this._parsedPaths = new ListEx<PropertyBinding.ParseResult>(); // inside: { we don"t care, here }
            this._bindings = new ListEx<ListEx<PropertyBinding>>(); // inside: Array< PropertyBinding >
            this._bindingsIndicesByPath = new JsObj<int?>(); // inside: indices in these arrays
            var scope = this;
        }

        #endregion

        #region properties

        #endregion

        #region methods
        public void add(params Object3D[] arguments)
        {
            var objects = this._objects;
            var indicesByUUID = this._indicesByUUID;
            var paths = this._paths;
            var parsedPaths = this._parsedPaths;
            var bindings = this._bindings;
            var nBindings = bindings.Length;
            Object3D knownObject = null;
            var nObjects = objects.Length;
            var nCachedObjects = this.nCachedObjects_;
            for (int i = 0, n = arguments.Length; i != n; ++i)
            {
                var _object = arguments[i];
                var uuid = _object.uuid;
                var index = indicesByUUID[uuid] ?? -1;
                if (index == -1)//index == undefined)
                {
                    // unknown object -> add it to the ACTIVE region
                    index = nObjects++;
                    indicesByUUID[uuid] = index;
                    objects.Push(_object);
                    // accounting is done, now do the same for all bindings
                    for (int j = 0, m = nBindings; j != m; ++j)
                    {
                        bindings[j].Push(new PropertyBinding(_object, paths[j], parsedPaths[j]));
                    }
                }
                else if (index < nCachedObjects)
                {
                    knownObject = objects[index];
                    // move existing object to the ACTIVE region
                    var firstActiveIndex = --nCachedObjects;
                    var lastCachedObject = objects[firstActiveIndex];
                    indicesByUUID[lastCachedObject.uuid] = index;
                    objects[index] = lastCachedObject;
                    indicesByUUID[uuid] = firstActiveIndex;
                    objects[firstActiveIndex] = _object;
                    // accounting is done, now do the same for all bindings
                    for (int j = 0, m = nBindings; j != m; ++j)
                    {
                        var bindingsForPath = bindings[j];
                        var lastCached = bindingsForPath[firstActiveIndex];
                        var binding = bindingsForPath[index];
                        bindingsForPath[index] = lastCached;
                        if (binding == null)
                        {
                            // since we do not bother to create new bindings
                            // for objects that are cached, the binding may
                            // or may not exist
                            binding = new PropertyBinding(_object, paths[j], parsedPaths[j]);
                        }
                        bindingsForPath[firstActiveIndex] = binding;
                    }
                }
                else if (objects[index] != knownObject)
                {
                    console.error("THREE.AnimationObjectGroup: Different objects with the same UUID " +
                        "detected. Clean the caches or recreate your infrastructure when reloading scenes.");
                } // else the object is already where we want it to be
            } // for arguments
            this.nCachedObjects_ = nCachedObjects;
        }
        public void remove(params Object3D[] arguments)
        {
            var objects = this._objects;
            var indicesByUUID = this._indicesByUUID;
            var bindings = this._bindings;
            var nBindings = bindings.Length;
            var nCachedObjects = this.nCachedObjects_;
            for (int i = 0, n = arguments.Length; i != n; ++i)
            {
                var _object = arguments[i];
                var uuid = _object.uuid;
                var index = indicesByUUID[uuid] ?? -1;
                if (index != -1 && index >= nCachedObjects)
                {
                    // move existing object into the CACHED region
                    var lastCachedIndex = nCachedObjects++;
                    var firstActiveObject = objects[lastCachedIndex];
                    indicesByUUID[firstActiveObject.uuid] = index;
                    objects[index] = firstActiveObject;
                    indicesByUUID[uuid] = lastCachedIndex;
                    objects[lastCachedIndex] = _object;
                    // accounting is done, now do the same for all bindings
                    for (int j = 0, m = nBindings; j != m; ++j)
                    {
                        var bindingsForPath = bindings[j];
                        var firstActive = bindingsForPath[lastCachedIndex];
                        var binding = bindingsForPath[index];
                        bindingsForPath[index] = firstActive;
                        bindingsForPath[lastCachedIndex] = binding;
                    }
                }
            } // for arguments
            this.nCachedObjects_ = nCachedObjects;
        }
        public void uncache(params object[] arguments)
        {
            var objects = this._objects;
            var indicesByUUID = this._indicesByUUID;
            var bindings = this._bindings;
            var nBindings = bindings.Length;
            var nCachedObjects = this.nCachedObjects_;
            var nObjects = objects.Length;
            for (int i = 0, n = arguments.Length; i != n; ++i)
            {
                var _object = arguments[i] as Object3D;
                var uuid = _object.uuid;
                var index = indicesByUUID[uuid] ?? -1;
                if (index != -1)
                {
                    indicesByUUID.remove(uuid);
                    if (index < nCachedObjects)
                    {
                        // object is cached, shrink the CACHED region
                        var firstActiveIndex = --nCachedObjects;
                        var lastCachedObject = objects[firstActiveIndex];
                        var lastIndex = --nObjects;
                        var lastObject = objects[lastIndex];
                        // last cached object takes this object"s place
                        indicesByUUID[lastCachedObject.uuid] = index;
                        objects[index] = lastCachedObject;
                        // last object goes to the activated slot and pop
                        indicesByUUID[lastObject.uuid] = firstActiveIndex;
                        objects[firstActiveIndex] = lastObject;
                        objects.Pop();
                        // accounting is done, now do the same for all bindings
                        for (int j = 0, m = nBindings; j != m; ++j)
                        {
                            var bindingsForPath = bindings[j];
                            var lastCached = bindingsForPath[firstActiveIndex];
                            var last = bindingsForPath[lastIndex];
                            bindingsForPath[index] = lastCached;
                            bindingsForPath[firstActiveIndex] = last;
                            bindingsForPath.Pop();
                        }
                    }
                    else
                    {
                        // object is active, just swap with the last and pop
                        var lastIndex = --nObjects;
                        var lastObject = objects[lastIndex];
                        if (lastIndex > 0)
                        {
                            indicesByUUID[lastObject.uuid] = index;
                        }
                        objects[index] = lastObject;
                        objects.Pop();
                        // accounting is done, now do the same for all bindings
                        for (int j = 0, m = nBindings; j != m; ++j)
                        {
                            var bindingsForPath = bindings[j];
                            bindingsForPath[index] = bindingsForPath[lastIndex];
                            bindingsForPath.Pop();
                        }
                    } // cached or active
                } // if object is known
            } // for arguments
            this.nCachedObjects_ = nCachedObjects;
        }
        public ListEx<PropertyBinding> subscribe_(string path, PropertyBinding.ParseResult parsedPath)
        {
            // returns an array of bindings for the given path that is changed
            // according to the contained objects in the group
            var indicesByPath = this._bindingsIndicesByPath;
            var index = indicesByPath[path] ?? -1;
            var bindings = this._bindings;
            if (indicesByPath.ContainsKey(path) && index != -1) return bindings[index];
            var paths = this._paths;
            var parsedPaths = this._parsedPaths;
            var objects = this._objects;
            var nObjects = objects.Length;
            var nCachedObjects = this.nCachedObjects_;
            var bindingsForPath = new ListEx<PropertyBinding>(nObjects);
            index = bindings.Length;
            indicesByPath[path] = index;
            paths.Push(path);
            parsedPaths.Push(parsedPath);
            bindings.Push(bindingsForPath);
            for (int i = nCachedObjects, n = objects.Length; i != n; ++i)
            {
                var _object = objects[i];
                bindingsForPath[i] = new PropertyBinding(_object, path, parsedPath);
            }
            return bindingsForPath;
        }
        public void unsubscribe_(string path)
        {
            // tells the group to forget about a property path and no longer
            // update the array previously obtained with "subscribe_"
            var indicesByPath = this._bindingsIndicesByPath;
            var index = indicesByPath[path] ?? -1;
            if (indicesByPath.ContainsKey(path) && index != -1)
            {
                var paths = this._paths;
                var parsedPaths = this._parsedPaths;
                var bindings = this._bindings;
                var lastBindingsIndex = bindings.Length - 1;
                var lastBindings = bindings[lastBindingsIndex];
                var lastBindingsPath = path[lastBindingsIndex].ToString();
                indicesByPath[lastBindingsPath] = index;
                bindings[index] = lastBindings;
                bindings.Pop();
                parsedPaths[index] = parsedPaths[lastBindingsIndex];
                parsedPaths.Pop();
                paths[index] = paths[lastBindingsIndex];
                paths.Pop();
            }
        }
        #endregion

    }
}
